;(function(root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery'], factory);
    } else if (typeof exports === 'object') {
        module.exports = factory(require('jquery'));
    } else {
        root.sortable = factory(root.jQuery);
    }
}(this, function($) {
    /*
     * HTML5 Sortable jQuery Plugin
     * https://github.com/voidberg/html5sortable
     *
     * Original code copyright 2012 Ali Farhadi.
     * This version is mantained by Alexandru Badiu <andu@ctrlz.ro> & Lukas Oppermann <lukas@vea.re>
     *
     *
     * Released under the MIT license.
     */
    'use strict';
    /*
     * variables global to the plugin
     */
    var dragging;
    var draggingHeight;
    var placeholders = $();
    var sortables = [];
    /*
     * remove event handlers from items
     * @param [jquery Collection] items
     * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event)
     */
    var _removeItemEvents = function(items) {
        items.off('dragstart.h5s');
        items.off('dragend.h5s');
        items.off('selectstart.h5s');
        items.off('dragover.h5s');
        items.off('dragenter.h5s');
        items.off('drop.h5s');
    };
    /*
     * remove event handlers from sortable
     * @param [jquery Collection] sortable
     * @info event.h5s (jquery way of namespacing events, to bind multiple handlers to the event)
     */
    var _removeSortableEvents = function(sortable) {
        sortable.off('dragover.h5s');
        sortable.off('dragenter.h5s');
        sortable.off('drop.h5s');
    };
    /*
     * attache ghost to dataTransfer object
     * @param [event] original event
     * @param [object] ghost-object with item, x and y coordinates
     */
    var _attachGhost = function(event, ghost) {
        // this needs to be set for HTML5 drag & drop to work
        event.dataTransfer.effectAllowed = 'move';
        event.dataTransfer.setData('text', '');

        // check if setDragImage method is available
        if (event.dataTransfer.setDragImage) {
            event.dataTransfer.setDragImage(ghost.item, ghost.x, ghost.y);
        }
    };
    /**
     * _addGhostPos clones the dragged item and adds it as a Ghost item
     * @param [object] event - the event fired when dragstart is triggered
     * @param [object] ghost - .item = node, draggedItem = jQuery collection
     */
    var _addGhostPos = function(e, ghost) {
        if (!ghost.x) {
            ghost.x = parseInt(e.pageX - ghost.draggedItem.offset().left);
        }
        if (!ghost.y) {
            ghost.y = parseInt(e.pageY - ghost.draggedItem.offset().top);
        }
        return ghost;
    };
    /**
     * _makeGhost decides which way to make a ghost and passes it to attachGhost
     * @param [jQuery selection] $draggedItem - the item that the user drags
     */
    var _makeGhost = function($draggedItem) {
        return {
            item: $draggedItem[0],
            draggedItem: $draggedItem
        };
    };
    /**
     * _getGhost constructs ghost and attaches it to dataTransfer
     * @param [event] event - the original drag event object
     * @param [jQuery selection] $draggedItem - the item that the user drags
     * @param [object] ghostOpt - the ghost options
     */
// TODO: could $draggedItem be replaced by event.target in all instances
    var _getGhost = function(event, $draggedItem) {
        // add ghost item & draggedItem to ghost object
        var ghost = _makeGhost($draggedItem);
        // attach ghost position
        ghost = _addGhostPos(event, ghost);
        // attach ghost to dataTransfer
        _attachGhost(event, ghost);
    };
    /*
     * return options if not set on sortable already
     * @param [object] soptions
     * @param [object] options
     */
    var _getOptions = function(soptions, options) {
        if (typeof soptions === 'undefined') {
            return options;
        }
        return soptions;
    };
    /*
     * remove data from sortable
     * @param [jquery Collection] a single sortable
     */
    var _removeSortableData = function(sortable) {
        sortable.removeData('opts');
        sortable.removeData('connectWith');
        sortable.removeData('items');
        sortable.removeAttr('aria-dropeffect');
    };
    /*
     * remove data from items
     * @param [jquery Collection] items
     */
    var _removeItemData = function(items) {
        items.removeAttr('aria-grabbed');
        items.removeAttr('draggable');
        items.removeAttr('role');
    };
    /*
     * check if two lists are connected
     * @param [jquery Collection] items
     */
    var _listsConnected = function(curList, destList) {
        if (curList[0] === destList[0]) {
            return true;
        }
        if (curList.data('connectWith') !== undefined) {
            return curList.data('connectWith') === destList.data('connectWith');
        }
        return false;
    };
    /*
     * destroy the sortable
     * @param [jquery Collection] a single sortable
     */
    var _destroySortable = function(sortable) {
        var opts = sortable.data('opts') || {};
        var items = sortable.children(opts.items);
        var handles = opts.handle ? items.find(opts.handle) : items;
        // remove event handlers & data from sortable
        _removeSortableEvents(sortable);
        _removeSortableData(sortable);
        // remove event handlers & data from items
        handles.off('mousedown.h5s');
        _removeItemEvents(items);
        _removeItemData(items);
    };
    /*
     * enable the sortable
     * @param [jquery Collection] a single sortable
     */
    var _enableSortable = function(sortable) {
        var opts = sortable.data('opts');
        var items = sortable.children(opts.items);
        var handles = opts.handle ? items.find(opts.handle) : items;
        sortable.attr('aria-dropeffect', 'move');
        handles.attr('draggable', 'true');
        // IE FIX for ghost
        // can be disabled as it has the side effect that other events
        // (e.g. click) will be ignored
        var spanEl = (document || window.document).createElement('span');
        if (typeof spanEl.dragDrop === 'function' && !opts.disableIEFix) {
            handles.on('mousedown.h5s', function() {
                if (items.index(this) !== -1) {
                    this.dragDrop();
                } else {
                    $(this).parents(opts.items)[0].dragDrop();
                }
            });
        }
    };
    /*
     * disable the sortable
     * @param [jquery Collection] a single sortable
     */
    var _disableSortable = function(sortable) {
        var opts = sortable.data('opts');
        var items = sortable.children(opts.items);
        var handles = opts.handle ? items.find(opts.handle) : items;
        sortable.attr('aria-dropeffect', 'none');
        handles.attr('draggable', false);
        handles.off('mousedown.h5s');
    };
    /*
     * reload the sortable
     * @param [jquery Collection] a single sortable
     * @description events need to be removed to not be double bound
     */
    var _reloadSortable = function(sortable) {
        var opts = sortable.data('opts');
        var items = sortable.children(opts.items);
        var handles = opts.handle ? items.find(opts.handle) : items;
        // remove event handlers from items
        _removeItemEvents(items);
        handles.off('mousedown.h5s');
        // remove event handlers from sortable
        _removeSortableEvents(sortable);
    };
    /*
     * public sortable object
     * @param [object|string] options|method
     */
    var sortable = function(selector, options) {

        var $sortables = $(selector);
        var method = String(options);

        options = $.extend({
            connectWith: false,
            placeholder: null,
            // dragImage can be null or a jQuery element
            dragImage: null,
            disableIEFix: false,
            placeholderClass: 'sortable-placeholder',
            draggingClass: 'sortable-dragging',
            hoverClass: false
        }, options);

        /* TODO: maxstatements should be 25, fix and remove line below */
        /*jshint maxstatements:false */
        return $sortables.each(function() {

            var $sortable = $(this);

            if (/enable|disable|destroy/.test(method)) {
                sortable[method]($sortable);
                return;
            }

            // get options & set options on sortable
            options = _getOptions($sortable.data('opts'), options);
            $sortable.data('opts', options);
            // reset sortable
            _reloadSortable($sortable);
            // initialize
            var items = $sortable.children(options.items);
            var index;
            var startParent;
            var newParent;
            var placeholder = (options.placeholder === null) ? $('<' + (/^ul|ol$/i.test(this.tagName) ? 'li' : 'div') + ' class="' + options.placeholderClass + '"/>') : $(options.placeholder).addClass(options.placeholderClass);

            // setup sortable ids
            if (!$sortable.attr('data-sortable-id')) {
                var id = sortables.length;
                sortables[id] = $sortable;
                $sortable.attr('data-sortable-id', id);
                items.attr('data-item-sortable-id', id);
            }

            $sortable.data('items', options.items);
            placeholders = placeholders.add(placeholder);
            if (options.connectWith) {
                $sortable.data('connectWith', options.connectWith);
            }

            _enableSortable($sortable);
            items.attr('role', 'option');
            items.attr('aria-grabbed', 'false');

            // Mouse over class
            if (options.hoverClass) {
                var hoverClass = 'sortable-over';
                if (typeof options.hoverClass === 'string') {
                    hoverClass = options.hoverClass;
                }

                items.hover(function() {
                    $(this).addClass(hoverClass);
                }, function() {
                    $(this).removeClass(hoverClass);
                });
            }

            // Handle drag events on draggable items
            items.on('dragstart.h5s', function(e) {
                e.stopImmediatePropagation();

                if (options.dragImage) {
                    _attachGhost(e.originalEvent, {
                        item: options.dragImage,
                        x: 0,
                        y: 0
                    });
                    console.log('WARNING: dragImage option is deprecated' +
                        ' and will be removed in the future!');
                } else {
                    // add transparent clone or other ghost to cursor
                    _getGhost(e.originalEvent, $(this), options.dragImage);
                }
                // cache selsection & add attr for dragging
                dragging = $(this);
                dragging.addClass(options.draggingClass);
                dragging.attr('aria-grabbed', 'true');
                // grab values
                index = dragging.index();
                draggingHeight = dragging.height();
                startParent = $(this).parent();
                // trigger sortstar update
                dragging.parent().triggerHandler('sortstart', {
                    item: dragging,
                    placeholder: placeholder,
                    startparent: startParent
                });
            });
            // Handle drag events on draggable items
            items.on('dragend.h5s', function() {
                if (!dragging) {
                    return;
                }
                // remove dragging attributes and show item
                dragging.removeClass(options.draggingClass);
                dragging.attr('aria-grabbed', 'false');
                dragging.show();

                placeholders.detach();
                newParent = $(this).parent();
                dragging.parent().triggerHandler('sortstop', {
                    item: dragging,
                    startparent: startParent,
                });
                if (index !== dragging.index() ||
                    startParent.get(0) !== newParent.get(0)) {
                    dragging.parent().triggerHandler('sortupdate', {
                        item: dragging,
                        index: newParent.children(newParent.data('items')).index(dragging),
                        oldindex: items.index(dragging),
                        elementIndex: dragging.index(),
                        oldElementIndex: index,
                        startparent: startParent,
                        endparent: newParent
                    });
                }
                dragging = null;
                draggingHeight = null;
            });
            // Handle drop event on sortable & placeholder
            // TODO: REMOVE placeholder?????
            $(this).add([placeholder]).on('drop.h5s', function(e) {
                if (!_listsConnected($sortable, $(dragging).parent())) {
                    return;
                }

                e.stopPropagation();
                placeholders.filter(':visible').after(dragging);
                dragging.trigger('dragend.h5s');
                return false;
            });

            // Handle dragover and dragenter events on draggable items
            items.add([this]).on('dragover.h5s dragenter.h5s', function(e) {
                if (!_listsConnected($sortable, $(dragging).parent())) {
                    return;
                }

                e.preventDefault();
                e.originalEvent.dataTransfer.dropEffect = 'move';
                if (items.is(this)) {
                    var thisHeight = $(this).height();
                    if (options.forcePlaceholderSize) {
                        placeholder.height(draggingHeight);
                    }

                    // Check if $(this) is bigger than the draggable. If it is, we have to define a dead zone to prevent flickering
                    if (thisHeight > draggingHeight) {
                        // Dead zone?
                        var deadZone = thisHeight - draggingHeight;
                        var offsetTop = $(this).offset().top;
                        if (placeholder.index() < $(this).index() &&
                            e.originalEvent.pageY < offsetTop + deadZone) {
                            return false;
                        }
                        if (placeholder.index() > $(this).index() &&
                            e.originalEvent.pageY > offsetTop + thisHeight - deadZone) {
                            return false;
                        }
                    }

                    dragging.hide();
                    if (placeholder.index() < $(this).index()) {
                        $(this).after(placeholder);
                    } else {
                        $(this).before(placeholder);
                    }
                    placeholders.not(placeholder).detach();
                } else {
                    if (!placeholders.is(this) && !$(this).children(options.items).length) {
                        placeholders.detach();
                        $(this).append(placeholder);
                    }
                }
                return false;
            });
        });
    };

    sortable.destroy = function(sortable) {
        _destroySortable(sortable);
    };

    sortable.enable = function(sortable) {
        _enableSortable(sortable);
    };

    sortable.disable = function(sortable) {
        _disableSortable(sortable);
    };

    $.fn.sortable = function(options) {
        return sortable(this, options);
    };

    return sortable;
}));